package org.dromara.common.mail.utils;

import cn.hutool.core.builder.Builder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.mail.*;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import jakarta.mail.util.ByteArrayDataSource;

import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：邮件发送客户端
 */
/**
 * 邮件构建器类，实现了Builder接口，用于构建MimeMessage类型的邮件对象
 */
public class Mail implements Builder<MimeMessage> {
    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 邮箱帐户信息以及一些客户端配置信息
     */
    private final MailAccount mailAccount;

    /**
     * 收件人列表
     */
    private String[] tos;

    /**
     * 抄送人列表（carbon copy）
     */
    private String[] ccs;

    /**
     * 密送人列表（blind carbon copy）
     */
    private String[] bccs;

    /**
     * 回复地址(reply-to)
     */
    private String[] reply;

    /**
     * 标题
     */
    private String title;

    /**
     * 内容
     */
    private String content;

    /**
     * 是否为HTML
     */
    private boolean isHtml;

    /**
     * 正文、附件和图片的混合部分
     */
    private final Multipart multipart = new MimeMultipart();

    /**
     * 是否使用全局会话，默认为false
     */
    private boolean useGlobalSession = false;

    /**
     * debug输出位置，可以自定义debug日志
     */
    private PrintStream debugOutput;

    /**
     * 创建邮件客户端
     *
     * @param mailAccount 邮件帐号
     * @return Mail 返回当前Mail实例，以便支持链式调用
     */
    public static Mail create(MailAccount mailAccount) {
        return new Mail(mailAccount);
    }

    /**
     * 创建邮件客户端，使用全局邮件帐户
     *
     * @return Mail 返回当前Mail实例，以便支持链式调用
     */
    public static Mail create() {
        return new Mail();
    }

    // --------------------------------------------------------------- Constructor start

    /**
     * 构造，使用全局邮件帐户
     */
    public Mail() {
        this(GlobalMailAccount.INSTANCE.getAccount());
    }

    /**
     * 构造
     *
     * @param mailAccount 邮件帐户，如果为null使用默认配置文件的全局邮件配置
     */
    public Mail(MailAccount mailAccount) {
        // 判断传入的邮件账户是否为空，如果为空则使用全局的邮件账户
        mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
        // 设置邮件账户，如果账户为空则使用默认配置
        this.mailAccount = mailAccount.defaultIfEmpty();
    }
    // --------------------------------------------------------------- Constructor end

    // --------------------------------------------------------------- Getters and Setters start

    /**
     * 设置收件人
     *
     * @param tos 收件人列表
     * @return this 返回当前Mail实例，以便支持链式调用
     * @see #setTos(String...)
     */
    public Mail to(String... tos) {
        return setTos(tos);
    }

    /**
     * 设置多个收件人
     *
     * @param tos 收件人列表
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setTos(String... tos) {
        this.tos = tos;
        return this;
    }

    /**
     * 设置多个抄送人（carbon copy）
     *
     * @param ccs 抄送人列表
     * @return this 返回当前Mail实例，以便支持链式调用
     * @since 4.0.3
     */
    public Mail setCcs(String... ccs) {
        this.ccs = ccs;
        return this;
    }

    /**
     * 设置多个密送人（blind carbon copy）
     *
     * @param bccs 密送人列表
     * @return this 返回当前Mail实例，以便支持链式调用
     * @since 4.0.3
     */
    public Mail setBccs(String... bccs) {
        this.bccs = bccs;
        return this;
    }

    /**
     * 设置多个回复地址(reply-to)
     *
     * @param reply 回复地址(reply-to)列表
     * @return this 返回当前Mail实例，以便支持链式调用
     * @since 4.6.0
     */
    public Mail setReply(String... reply) {
        this.reply = reply;
        return this;
    }

    /**
     * 设置标题
     *
     * @param title 标题
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setTitle(String title) {
        this.title = title;
        return this;
    }

    /**
     * 设置正文<br>
     * 正文可以是普通文本也可以是HTML（默认普通文本），可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
     *
     * @param content 正文
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setContent(String content) {
        this.content = content;
        return this;
    }

    /**
     * 设置是否是HTML
     *
     * @param isHtml 是否为HTML
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setHtml(boolean isHtml) {
        this.isHtml = isHtml;
        return this;
    }

    /**
     * 设置正文
     *
     * @param content 正文内容
     * @param isHtml  是否为HTML
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setContent(String content, boolean isHtml) {
        setContent(content);
        return setHtml(isHtml);
    }

    /**
     * 设置邮件的附件
     * 设置文件类型附件，文件可以是图片文件，此时自动设置cid（正文中引用图片），默认cid为文件名
     *
     * @param files 附件文件列表 要作为附件添加的文件数组
     * @return this 返回当前Mail实例，以便支持链式调用
     */
    public Mail setFiles(File... files) {
        // 如果文件数组为空或null，则不进行任何操作，直接返回当前实例
        if (ArrayUtil.isEmpty(files)) {
            return this;
        }

        // 创建一个与文件数组长度相同的DataSource数组，用于存储每个文件的DataSource对象
        final DataSource[] attachments = new DataSource[files.length];
        // 遍历文件数组，为每个文件创建一个FileDataSource对象，并将其存储到attachments数组中
        for (int i = 0; i < files.length; i++) {
            attachments[i] = new FileDataSource(files[i]);
        }
        // 调用setAttachments方法，将创建的DataSource数组作为附件设置到邮件中，并返回当前Mail实例
        return setAttachments(attachments);
    }

    /**
     * 设置邮件附件
     * 增加附件或图片，附件使用{@link DataSource} 形式表示，可以使用{@link FileDataSource}包装文件表示文件附件
     *
     * @param attachments 附件列表 附件数据源数组，可以包含多个附件
     * @return this 返回当前Mail实例，支持链式调用
     * @since 4.0.9
     */
    public Mail setAttachments(DataSource... attachments) {
        // 检查附件数组是否非空
        if (ArrayUtil.isNotEmpty(attachments)) {
            // 获取邮件账户设置的字符集
            final Charset charset = this.mailAccount.getCharset();
            // 创建一个新的MIME体部分用于包含附件
            MimeBodyPart bodyPart;
            // 用于存储编码后的文件名
            String nameEncoded;
            try {
                // 遍历每个附件数据源
                for (DataSource attachment : attachments) {
                    bodyPart = new MimeBodyPart();
                    // 设置附件的内容处理器
                    bodyPart.setDataHandler(new DataHandler(attachment));
                    // 获取附件的文件名
                    nameEncoded = attachment.getName();
                    // 如果邮件账户设置要求对文件名进行编码
                    if (this.mailAccount.isEncodefilename()) {
                        // 对文件名进行编码
                        nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
                    }
                    // 普通附件文件名
                    bodyPart.setFileName(nameEncoded);
                    // 如果附件是图片类型
                    if (StrUtil.startWith(attachment.getContentType(), "image/")) {
                        // 图片附件，用于正文中引用图片
                        bodyPart.setContentID(nameEncoded);
                    }
                    // 将创建的包含附件信息的MIME体部分添加到multipart中
                    this.multipart.addBodyPart(bodyPart);
                }
            } catch (MessagingException e) {
                throw new MailException(e);
            }
        }
        return this;
    }

    /**
     * 增加图片，图片的键对应到邮件模板中的占位字符串，图片类型默认为"image/jpeg"
     * 此方法允许用户向邮件中添加一个图片，使用CID（Content-ID）作为图片的唯一标识符，和一个输入流来提供图片数据
     * 如果不需要额外的图片信息（如文件名），可以使用此方法的重载版本，它只接受CID和输入流作为参数
     *
     * @param cid         图片与占位符，占位符格式为cid:${cid} 图片的Content-ID，用于邮件中引用该图片
     * @param imageStream 图片文件 图片数据的输入流，可以是文件输入流、网络输入流等
     * @return this 返回更新后的邮件对象，以便支持链式调用
     * @since 4.6.3
     */
    public Mail addImage(String cid, InputStream imageStream) {
        return addImage(cid, imageStream, null);
    }

    /**
     * 增加图片，图片的键对应到邮件模板中的占位字符串
     *
     * @param cid         图片与占位符，占位符格式为cid:${cid} 图片的Content-ID，用于在邮件正文中引用图片
     * @param imageStream 图片流，不关闭 图片数据流，包含要添加的图片数据
     * @param contentType 图片类型，null赋值默认的"image/jpeg"
     * @return this 返回更新后的Mail对象，包含新增的图片附件
     * @since 4.6.3
     */
    public Mail addImage(String cid, InputStream imageStream, String contentType) {
        // 创建ByteArrayDataSource对象以存储图片数据，如果contentType为null，则默认使用"image/jpeg"
        ByteArrayDataSource imgSource;
        try {
            imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
        } catch (IOException e) {
            // 如果在读取图片数据时发生IO异常，将其转换为IORuntimeException抛出
            throw new IORuntimeException(e);
        }
        // 为数据源设置一个名称，即cid，以便在邮件中引用
        imgSource.setName(cid);
        // 将图片数据源作为附件添加到邮件中，并返回更新后的Mail对象
        return setAttachments(imgSource);
    }

    /**
     * 增加图片，图片的键对应到邮件模板中的占位字符串
     *
     * @param cid       图片与占位符，占位符格式为cid:${cid}
     * @param imageFile 图片文件
     * @return this 返回更新后的邮件对象
     * @since 4.6.3
     */
    public Mail addImage(String cid, File imageFile) {
        InputStream in = null;
        try {
            // 获取图片文件的输入流
            in = FileUtil.getInputStream(imageFile);
            // 添加图片到邮件中，使用输入流和文件类型映射来确定内容类型
            return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
        } finally {
            // 确保关闭输入流以避免资源泄漏
            IoUtil.close(in);
        }
    }

    /**
     * 设置字符集编码 设置邮件的字符集
     *
     * @param charset 字符集编码
     * @return this Mail对象本身，支持链式调用
     * @see MailAccount#setCharset(Charset)
     */
    public Mail setCharset(Charset charset) {
        this.mailAccount.setCharset(charset);
        return this;
    }

    /**
     * 设置是否使用全局会话，默认为true
     *
     * @param isUseGlobalSession 是否使用全局会话，默认为true
     * @return this Mail对象本身，支持链式调用
     * @since 4.0.2
     */
    public Mail setUseGlobalSession(boolean isUseGlobalSession) {
        this.useGlobalSession = isUseGlobalSession;
        return this;
    }

    /**
     * 设置debug输出位置，可以自定义debug日志
     *
     * @param debugOutput debug输出位置
     * @return this Mail对象本身，支持链式调用
     * @since 5.5.6
     */
    public Mail setDebugOutput(PrintStream debugOutput) {
        this.debugOutput = debugOutput;
        return this;
    }
    // --------------------------------------------------------------- Getters and Setters end

    /**
     * 构建并返回一个MimeMessage对象
     * 此方法用于封装构建邮件消息的过程，以便在发生异常时能够统一处理
     *
     * @return MimeMessage 构建完成的邮件消息对象
     * @throws MailException 当构建邮件消息过程中发生MessagingException异常时，转换为MailException抛出
     */
    @Override
    public MimeMessage build() {
        try {
            // 调用具体实现方法构建邮件消息
            return buildMsg();
        } catch (MessagingException e) {
            throw new MailException(e);
        }
    }

    /**
     * 发送
     * 发送邮件
     *
     * 此方法封装了实际的邮件发送过程，并处理发送中可能遇到的异常
     * 如果邮件发送成功，返回邮件发送的相关信息
     * 如果邮件发送失败，根据不同的异常类型提供更详细的错误信息
     *
     * @return message-id
     * @throws MailException 邮件发送异常
     */
    public String send() throws MailException {
        try {
            // 尝试执行邮件发送操作
            return doSend();
        } catch (MessagingException e) {
            if (e instanceof SendFailedException) {
                // 当地址无效时，显示更加详细的无效地址信息
                final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
                final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
                throw new MailException(msg, e);
            }
            throw new MailException(e);
        }
    }

    // --------------------------------------------------------------- Private method start

    /**
     * 执行发送
     * 发送邮件
     * 该方法构建并发送一个MIME邮件对象，不接收任何参数
     *
     * @return message-id 发送的邮件的消息ID
     * @throws MessagingException 发送异常
     */
    private String doSend() throws MessagingException {
        // 构建邮件对象
        final MimeMessage mimeMessage = buildMsg();

        // 发送邮件
        Transport.send(mimeMessage);

        // 返回邮件的消息ID
        return mimeMessage.getMessageID();
    }

    /**
     * 构建消息 构建邮件消息
     *
     * @return {@link MimeMessage}消息
     * @throws MessagingException 消息异常
     */
    private MimeMessage buildMsg() throws MessagingException {
        // 使用提供的发件人信息设置发件人
        final Charset charset = this.mailAccount.getCharset();
        final MimeMessage msg = new MimeMessage(getSession());
        // 发件人
        final String from = this.mailAccount.getFrom();
        if (StrUtil.isEmpty(from)) {
            // 用户未提供发送方，则从Session中自动获取
            msg.setFrom();
        } else {
            // 使用提供的发件人信息设置发件人
            msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
        }
        // 标题
        msg.setSubject(this.title, (null == charset) ? null : charset.name());
        // 发送时间
        msg.setSentDate(new Date());
        // 内容和附件
        msg.setContent(buildContent(charset));
        // 收件人
        msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
        // 抄送人
        if (ArrayUtil.isNotEmpty(this.ccs)) {
            msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
        }
        // 密送人
        if (ArrayUtil.isNotEmpty(this.bccs)) {
            msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
        }
        // 回复地址(reply-to)
        if (ArrayUtil.isNotEmpty(this.reply)) {
            msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
        }

        return msg;
    }

    /**
     * 构建邮件信息主体
     * 构建邮件的多部分内容
     * 主要用于设置邮件正文，并将其添加到邮件的多部分内容中
     *
     * @param charset 编码，{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()} 字符集，用于指定邮件正文的字符集如果为null，则使用默认字符集
     * @return 邮件信息主体 返回构建好的多部分内容对象
     * @throws MessagingException 消息异常 如果发生邮件构建错误，将抛出此异常
     */
    private Multipart buildContent(Charset charset) throws MessagingException {
        // 根据提供的字符集或默认字符集，设置邮件正文的字符集
        final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
        // 创建一个新的 MimeBodyPart 对象，用于包含邮件的正文部分
        final MimeBodyPart body = new MimeBodyPart();
        // 设置邮件正文内容和类型，根据是否为 HTML 格式设置不同的 MIME 类型，并指定字符集
        body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
        // 将正文部分添加到多部分内容中
        this.multipart.addBodyPart(body);

        // 返回构建好的多部分内容对象
        return this.multipart;
    }

    /**
     * 获取默认邮件会话<br>
     * 如果为全局单例的会话，则全局只允许一个邮件帐号，否则每次发送邮件会新建一个新的会话
     * 此方法根据当前配置的邮件账户和是否使用全局会话的设置，来初始化和配置一个JavaMail会话对象
     * 如果调试输出已被设置，则将调试输出关联到会话
     *
     * @return 邮件会话 {@link Session}
     */
    private Session getSession() {
        // 根据当前邮件账户和是否使用全局会话的设置，获取会话对象
        final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);

        // 如果调试输出不为空，则将其设置到会话对象中
        if (null != this.debugOutput) {
            session.setDebugOut(debugOutput);
        }

        // 返回配置好的会话对象
        return session;
    }
    // --------------------------------------------------------------- Private method end
}
